/*
* Copyright 2013 Alex Lin.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.opoo.press.impl;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.LocaleUtils;
import org.apache.commons.lang.StringUtils;
import org.opoo.press.Collection;
import org.opoo.press.Converter;
import org.opoo.press.Factory;
import org.opoo.press.FileOrigin;
import org.opoo.press.Generator;
import org.opoo.press.Observer;
import org.opoo.press.Origin;
import org.opoo.press.OriginVisitor;
import org.opoo.press.Page;
import org.opoo.press.Post;
import org.opoo.press.ProcessorsProcessor;
import org.opoo.press.Renderer;
import org.opoo.press.Site;
import org.opoo.press.SiteBuilder;
import org.opoo.press.SiteConfig;
import org.opoo.press.Source;
import org.opoo.press.SourceDirectoryWalker;
import org.opoo.press.SourceVisitor;
import org.opoo.press.SourceWalker;
import org.opoo.press.StaticFile;
import org.opoo.press.Theme;
import org.opoo.press.ThemeCompiler;
import org.opoo.press.task.RunnableTask;
import org.opoo.press.task.TaskExecutor;
import org.opoo.press.util.PageUtils;
import org.opoo.press.util.StaleUtils;
import org.opoo.util.PathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* @author Alex Lin
*/
public class SiteImpl implements Site, SiteBuilder {
private static final Logger log = LoggerFactory.getLogger(SiteImpl.class);
private SiteConfigImpl config;
private Map<String, Object> data;
private File dest;
private File templates;
private File working;
private File basedir;
private ValidDirList sources;
private ValidDirList assets;
private String root;
private List<StaticFile> staticFiles;
private Date time;
private boolean showDrafts = false;
private Renderer renderer;
private Locale locale;
private TaskExecutor taskExecutor;
private Theme theme;
private ProcessorsProcessor processors;
private ClassLoader classLoader;
private Factory factory;
private String dateFormatPattern;
private Map<String, Collection> collections;
;
private AllPages allPages;
private CacheManager cacheManager;
Cache<String, Object> pageCache;
public SiteImpl(SiteConfigImpl siteConfig) {
super();
this.config = siteConfig;
this.data = new HashMap<String, Object>(/*config*/);
this.basedir = config.getBasedir();
this.root = config.get("root", "");
// this.permalink = config.getPage("permalink");
this.showDrafts = config.get("show_drafts", false);
boolean debug = config.get("debug", false);
if (showDrafts) {
log.info("+ Show drafts option set 'ON'");
}
if (debug) {
for (Map.Entry<String, Object> en : config.entrySet()) {
String name = en.getKey();
name = StringUtils.leftPad(name, 25);
log.info(name + ": " + en.getValue());
}
}
//theme
theme = createTheme();
//templates
templates = theme.getTemplates();
log.debug("Template directory: {}", templates);
//sources
sources = new ValidDirList();
sources.addDir(theme.getSource());
List<String> sourcesConfig = config.get("source_dirs");
sources.addDirs(basedir, sourcesConfig);
log.debug("Source directories: {}", sources);
//assets
assets = new ValidDirList();
assets.addDir(theme.getAssets());
List<String> assetsConfig = config.get("asset_dirs");
assets.addDirs(basedir, assetsConfig);
log.debug("Assets directories: {}", assets);
//target directory
String destDir = config.get("dest_dir");
this.dest = PathUtils.appendBaseIfNotAbsolute(basedir, destDir);
log.debug("Destination directory: {}", dest);
//working directory
String workingDir = config.get("work_dir");
this.working = PathUtils.appendBaseIfNotAbsolute(basedir, workingDir);
log.debug("Working directory: {}", working);
reset();
setup();
}
private Theme createTheme() {
String name = config.get("theme", "default");
File themes = new File(config.getBasedir(), "themes");
File themeDir = PathUtils.appendBaseIfNotAbsolute(themes, name);
if (!themeDir.exists() || !themeDir.isDirectory()) {
throw new IllegalArgumentException("Theme directory not exists or not valid, please install theme first: "
+ themeDir);
}
// PathUtils.checkDir(themeDir, PathUtils.Strategy.THROW_EXCEPTION_IF_NOT_EXISTS);
compileTheme(themeDir);
return new ThemeImpl(themeDir, this);
}
private void compileTheme(File themeDir) {
ThemeCompiler themeCompiler = config.get("theme.compiler");
if (themeCompiler != null) {
log.debug("Compile theme by '{}'", themeCompiler.getClass().getName());
themeCompiler.compile(themeDir);
} else {
log.debug("no theme compiler found.");
}
}
public void build() {
build(false);
}
public void build(boolean force) {
if (force) {
log.info("force build.");
buildInternal();
return;
}
if (StaleUtils.isStale(this, false)) {
buildInternal();
return;
}
// only asset file(s) changed.
List<File> staleAssets = StaleUtils.getStaleAssets(this);
if (staleAssets != null) {
for (File staleAsset : staleAssets) {
//copy asset directory to destination directory
log.info("Copying stale asset: {}...", staleAsset);
try {
FileUtils.copyDirectory(staleAsset, dest, buildFilter());
StaleUtils.saveLastBuildInfo(this);
return;
} catch (IOException e) {
throw new RuntimeException("Copy stale asset exception: " + staleAsset, e);
}
}
}
log.info("Nothing to build - all site output files are up to date.");
}
@Override
public void clean() throws Exception {
log.info("Cleaning destination directory " + dest);
FileUtils.deleteDirectory(dest);
log.info("Cleaning working directory " + working);
FileUtils.deleteDirectory(working);
}
private void buildInternal() {
prepare();
reset();
read();
generate();
convert();
render();
cleanup();
write();
close();
StaleUtils.saveLastBuildInfo(this);
}
void prepare() {
boolean cache = config.get("cache", false);
if (cache) {
cacheManager = Caching.getCachingProvider().getCacheManager();
pageCache = cacheManager.getCache("pages");
if (pageCache == null) {
throw new IllegalArgumentException("'pages' cache not defined");
}
}
}
void close() {
if (cacheManager != null) {
pageCache.clear();
cacheManager.close();
cacheManager = null;
}
}
void reset() {
this.time = config.get("time", new Date());
//Call #add() in multi-threading
//this.allPages = Collections.synchronizedList( new ArrayList<Page>());
this.allPages = new AllPages();
this.collections = new LinkedHashMap<String, Collection>();
//Call #add() in multi-threading
this.staticFiles = Collections.synchronizedList(new ArrayList<StaticFile>());
}
void setup() {
//ensure source not in destination
for (File source : sources) {
source = PathUtils.canonical(source);
if (dest.equals(source) || source.getAbsolutePath().startsWith(dest.getAbsolutePath())) {
throw new IllegalArgumentException("Destination directory cannot be or contain the Source directory.");
}
}
//locale
String localeString = config.get("locale");
if (localeString != null) {
locale = LocaleUtils.toLocale(localeString);
log.debug("Set locale: " + locale);
}
//date_format
dateFormatPattern = config.get("date_format");
if (dateFormatPattern == null) {
dateFormatPattern = "yyyy-MM-dd";
} else if ("ordinal".equals(dateFormatPattern)) {
dateFormatPattern = "MMM d yyyy";
}
//object instances
classLoader = createClassLoader(config, theme);
taskExecutor = new TaskExecutor(config);
factory = FactoryImpl.createInstance(this);
processors = new ProcessorsProcessor(factory.getPluginManager().getProcessors());
//Construct RendererImpl after initializing all plugins
renderer = factory.getRenderer();
processors.postSetup(this);
}
private ClassLoader createClassLoader(SiteConfig config, Theme theme) {
log.debug("Create site ClassLoader.");
ClassLoader parent = SiteImpl.class.getClassLoader();
if (parent == null) {
parent = ClassLoader.getSystemClassLoader();
}
String sitePluginDir = config.get("plugin_dir");
String themePluginDir = theme.get("plugin_dir");
List<File> classPathEntries = new ArrayList<File>(2);
if (StringUtils.isNotBlank(sitePluginDir)) {
File sitePlugins = PathUtils.canonical(new File(config.getBasedir(), sitePluginDir));
addClassPathEntries(classPathEntries, sitePlugins);
}
if (StringUtils.isNotBlank(themePluginDir)) {
File themePlugins = PathUtils.canonical(new File(theme.getPath(), themePluginDir));
addClassPathEntries(classPathEntries, themePlugins);
}
//theme classes
File themeClasses = new File(theme.getPath(), "target/classes");
File themeSrc = new File(theme.getPath(), "src");
if (themeSrc.exists() && themeClasses.exists() && themeClasses.isDirectory()) {
classPathEntries.add(themeClasses);
}
//theme target/plugins
File themeTargetPlugins = new File(theme.getPath(), "target/plugins");
if (themeTargetPlugins.exists() && themeTargetPlugins.list().length > 0) {
addClassPathEntries(classPathEntries, themeTargetPlugins);
}
if (classPathEntries.isEmpty()) {
log.info("No custom classpath entries.");
return parent;
}
URL[] urls = new URL[classPathEntries.size()];
try {
for (int i = 0; i < classPathEntries.size(); i++) {
urls[i] = classPathEntries.get(i).toURI().toURL();
}
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
return new URLClassLoader(urls, parent);
}
private void addClassPathEntries(List<File> classPathEntries, File dir) {
if (dir.exists()) {
File[] files = dir.listFiles(new ValidPluginClassPathEntryFileFilter());
if (files != null && files.length > 0) {
classPathEntries.addAll(Arrays.asList(files));
}
}
}
void read() {
log.info("Reading sources...");
final FileFilter fileFilter = buildFilter();
final SourceDirectoryWalker sourceDirectoryWalker = factory.getSourceDirectoryWalker();
final SourceVisitor sourceVisitor = new SourceVisitor() {
@Override
public void visit(Source source) {
readSource(source);
}
@Override
public void visit(Origin origin) {
readStaticFile(origin);
}
};
final OriginVisitor staticFileVisitor = new OriginVisitor() {
@Override
public void visit(Origin origin) {
readStaticFile(origin);
}
};
List<Runnable> tasks = Lists.newArrayList();
for (final File src : sources) {
tasks.add(new Runnable() {
@Override
public void run() {
log.debug("Walk source: {}", src);
sourceDirectoryWalker.walk(src, fileFilter, sourceVisitor);
}
});
}
for (final File assetDir : assets) {
tasks.add(new Runnable() {
@Override
public void run() {
log.debug("Walk asset: {}", assetDir);
sourceDirectoryWalker.walk(assetDir, fileFilter, staticFileVisitor);
}
});
}
taskExecutor.run(tasks);
//All other walkers
List<SourceWalker> sourceWalkers = factory.getSourceWalkers();
if (sourceWalkers != null && !sourceWalkers.isEmpty()) {
for (SourceWalker walker : sourceWalkers) {
log.debug("Walk source by: {}", walker);
walker.walk(sourceVisitor);
}
}
processors.postRead(this);
log.debug("Read {} pages.", allPages.size());
if (log.isTraceEnabled()) {
for (Page page : allPages) {
System.out.println("==> " + page.getSource().getOrigin());
}
}
}
private void readStaticFile(Origin origin) {
log.debug("Reading static file {}", origin);
staticFiles.add(new StaticFileImpl(SiteImpl.this, origin));
}
// private SourceParser getSourceParser() {
// SourceParser sourceParser = factory.getSourceParser();
//
// if(cacheManager != null){
// log.debug("Use {} as SourceParser.", CachedSourceParserWrapper.class.getName());
// sourceParser = new CachedSourceParserWrapper(sourceParser, sourceCache, staticFileSourceEntryCache);
// }
// return sourceParser;
// }
private void readSource(Source src) {
Origin origin = src.getOrigin();
log.debug("Reading source [{}] {}", origin.getPath(), origin.getName());
Map<String, Object> map = src.getMeta();
String layout = (String) map.get("layout");
boolean draft = isDraft(map);
if (!draft || (draft && showDrafts)) {
Page page = factory.createPage(this, src, layout);
if (origin instanceof FileOrigin) {
try {
//path from basedir to source file
String pathFromBasedirToFile = PathUtils.getRelativePath(basedir, ((FileOrigin) origin).getSourceDirectory())
+ origin.getPath() + "/" + origin.getName();
page.set("pathFromBasedirToFile", pathFromBasedirToFile);
}catch (Exception e){
if(log.isTraceEnabled()){
log.trace("Process 'pathFromBasedirToFile' failed.", e);
}else{
log.debug("Process 'pathFromBasedirToFile' failed.");
}
}
}
//allPages.add(page);
addPage(page);
processors.postRead(this, page);
}
}
private boolean isDraft(Map<String, Object> meta) {
if (!meta.containsKey("published")) {
return false;
}
Boolean b = (Boolean) meta.get("published");
return !b.booleanValue();
}
FileFilter buildFilter() {
final List<String> includes = config.get("includes");
final List<String> excludes = config.get("excludes");
return new FileFilter() {
@Override
public boolean accept(File file) {
String name = file.getName();
if (includes != null && includes.contains(name)) {
return true;
}
if (excludes != null && excludes.contains(name)) {
return false;
}
char firstChar = name.charAt(0);
if (firstChar == '.' || firstChar == '_' || firstChar == '#') {
return false;
}
char lastChar = name.charAt(name.length() - 1);
if (lastChar == '~') {
return false;
}
if (file.isHidden()) {
return false;
}
return true;
}
};
}
void generate() {
log.info("Generating...");
for (Generator g : factory.getPluginManager().getGenerators()) {
g.generate(this);
}
processors.postGenerate(this);
}
void convert() {
log.info("Converting {} pages...", allPages.size());
taskExecutor.run(allPages, new RunnableTask<Page>() {
public void run(Page page) {
log.debug("Converting page: {}", page.getUrl());
Converter converter = getConverter(page.getSource());
page.convert(converter);
processors.postConvert(SiteImpl.this, page);
}
});
processors.postConvert(this);
}
void render() {
processors.preRender(this);
final Map<String, Object> rootMap = buildRootMap();
renderer.prepare();
log.info("Rendering {} pages...", allPages.size());
taskExecutor.run(allPages, new RunnableTask<Page>() {
public void run(Page page) {
log.debug("Rendering page: {}", page.getUrl());
page.render(renderer, factory.getHighlighter(), rootMap);
processors.postRender(SiteImpl.this, page);
}
});
processors.postRender(this);
}
Map<String, Object> buildRootMap() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("site", this);
map.put("root_url", getRoot());
map.put("basedir", getRoot());
map.put("opoopress", config.get("opoopress"));
map.put("theme", theme);
return map;
}
/**
*
*/
void cleanup() {
log.info("cleanup...");
final List<File> destFiles = getAllDestFiles(dest);
List<File> files = new ArrayList<File>();
for (StaticFile staticFile : staticFiles) {
files.add(staticFile.getOutputFile(dest));
}
log.debug("Files in target: {}", destFiles.size());
log.debug("Assets file in src: {}", files.size());
//find obsolete files
for (File file : files) {
destFiles.remove(file);
}
// destFiles.removeAll(files);
log.debug("Files in target will be deleted: {}", destFiles.size());
//delete obsolete files
if (!destFiles.isEmpty()) {
// for(File destFile: destFiles){
// //FileUtils.deleteQuietly(destFile);
// if(IS_DEBUG_ENABLED){
// log.debug("Delete file " + destFile);
// }
// }
taskExecutor.run(destFiles, new RunnableTask<File>() {
public void run(File file) {
FileUtils.deleteQuietly(file);
log.trace("File deleted: {}", file);
}
});
}
//call post cleanup
processors.postCleanup(this);
}
/**
* @param dest
* @return
*/
private List<File> getAllDestFiles(File dest) {
List<File> files = new ArrayList<File>();
if (dest != null && dest.exists()) {
listDestFiles(files, dest);
}
return files;
}
private void listDestFiles(List<File> files, File dir) {
File[] list = dir.listFiles();
for (File f : list) {
if (f.isFile()) {
files.add(f);
} else if (f.isDirectory()) {
listDestFiles(files, f);
}
}
}
void write() {
dest.mkdirs();
log.info("Writing {} pages to {}", allPages.size(), dest);
taskExecutor.run(allPages, new RunnableTask<Page>() {
@Override
public void run(Page input) {
input.write(dest);
}
});
if (!staticFiles.isEmpty()) {
log.info("Writing {} static files to {}...", staticFiles.size(), dest);
taskExecutor.run(staticFiles, new RunnableTask<StaticFile>() {
@Override
public void run(StaticFile input) {
input.write(dest);
}
});
}
processors.postWrite(this);
}
/**
* <pre>
* Collection collection = collections.get("page");
* return collection.getPages();
* </pre>
*
* @return the pages
* @deprecated
*/
public List<Page> getPages() {
Collection collection = collections.get("page");
if (collection != null) {
List<Page> pages = collection.getPages();
return ImmutableList.copyOf(pages);
}
return Collections.emptyList();
}
/**
* @return the posts
*/
public List<Post> getPosts() {
Collection collection = collections.get("post");
if (collection != null) {
List<Page> pages = collection.getPages();
return ImmutableList.copyOf(PageUtils.unwrap(pages, Post.class));
}
return Collections.emptyList();
}
@Override
public SiteConfig getConfig() {
return config;
}
@Override
public List<File> getSources() {
return sources;
}
@Override
public File getDestination() {
return dest;
}
public List<StaticFile> getStaticFiles() {
return staticFiles;
}
@Override
public List<Page> getAllPages() {
return allPages;
}
@Override
public synchronized Page addPage(Page page) {
allPages.addPage(page);
return page;
}
@Override
public Date getTime() {
return time;
}
@Override
public <T> T get(String name) {
if (data.containsKey(name)) {
return (T) data.get(name);
}
if (config.containsKey(name)) {
return config.get(name);
}
if (theme != null) {
return theme.get(name);
}
return null;
}
@Override
public <T> void set(String name, T value) {
data.put(name, value);
//MapUtils.put(data, name, value);
}
@Override
public Renderer getRenderer() {
return renderer;
}
@Override
public File getTemplates() {
return templates;
}
@Override
public List<File> getAssets() {
return assets;
}
@Override
public File getWorking() {
return working;
}
@Override
public Converter getConverter(Source source) {
return factory.getPluginManager().getConverter(source);
}
/* (non-Javadoc)
* @see org.opoo.press.Site#getRoot()
*/
@Override
public String getRoot() {
return root;
}
/* (non-Javadoc)
* @see org.opoo.press.Site#getLocale()
*/
@Override
public Locale getLocale() {
return locale;
}
@Override
public String getPermalink(String layout) {
if (layout == null) {
return null;
}
return config.get("permalink_" + layout);
}
/* (non-Javadoc)
* @see org.opoo.press.SiteHelper#toSlug(java.lang.String)
*/
@Override
public String toSlug(String tagName) {
return factory.getSlugHelper().toSlug(tagName);
}
/**
* @return the site
*/
@Override
public File getBasedir() {
return basedir;
}
@Override
public ClassLoader getClassLoader() {
return classLoader;
}
@Override
public Factory getFactory() {
return factory;
}
@Override
public Observer getObserver() {
return new SiteObserver(this);
}
/**
* @return the showDrafts
*/
public boolean showDrafts() {
return showDrafts;
}
/* (non-Javadoc)
* @see org.opoo.press.Site#getTheme()
*/
@Override
public Theme getTheme() {
return theme;
}
ProcessorsProcessor getProcessors() {
return processors;
}
@Override
public Map<String, Collection> getCollections() {
return collections;
}
@Override
public String formatDate(Date date) {
if (date != null) {
if (locale != null) {
return new SimpleDateFormat(dateFormatPattern, locale).format(date);
} else {
return new SimpleDateFormat(dateFormatPattern).format(date);
}
}
return null;
}
static class ValidDirList extends ArrayList<File> {
private static final long serialVersionUID = 6306507738477638252L;
public ValidDirList addDir(File dir) {
if (PathUtils.isValidDirectory(dir)) {
add(dir);
}
return this;
}
public ValidDirList addDir(File base, String path) {
return addDir(new File(base, path));
}
public ValidDirList addDirs(File base, List<String> paths) {
for (String path : paths) {
addDir(base, path);
}
return this;
}
}
static class ValidPluginClassPathEntryFileFilter implements FileFilter {
/* (non-Javadoc)
* @see java.io.FileFilter#accept(java.io.File)
*/
@Override
public boolean accept(File file) {
String name = file.getName();
char firstChar = name.charAt(0);
if (firstChar == '.' || firstChar == '_' || firstChar == '#') {
return false;
}
char lastChar = name.charAt(name.length() - 1);
if (lastChar == '~') {
return false;
}
if (file.isHidden()) {
return false;
}
if (file.isDirectory()) {
return true;
}
if (file.isFile()) {
name = name.toLowerCase();
if (name.endsWith(".jar") || name.endsWith(".zip")) {
return true;
}
}
return false;
}
}
static class AllPages extends ArrayList<Page> implements List<Page> {
public AllPages() {
super(100);
}
AllPages addPage(Page page) {
super.add(page);
return this;
}
@Override
@Deprecated
public boolean add(Page page) {
throw new UnsupportedOperationException("Call Site#addPage(Page) method instead.");
}
@Override
@Deprecated
public boolean addAll(java.util.Collection<? extends Page> c) {
throw new UnsupportedOperationException("Call Site#addPage(Page) method instead.");
}
@Override
@Deprecated
public boolean addAll(int index, java.util.Collection<? extends Page> c) {
throw new UnsupportedOperationException("Call Site#addPage(Page) method instead.");
}
@Override
@Deprecated
public Page set(int index, Page element) {
throw new UnsupportedOperationException("Call Site#addPage(Page) method instead.");
}
@Override
@Deprecated
public void add(int index, Page element) {
throw new UnsupportedOperationException("Call Site#addPage(Page) method instead.");
}
}
}